/*
 * Copyright 2009, Eniro Norge AS ( http://www.eniro.no)
 *
 * This software is the proprietary information of Eniro Norge AS.
 * Use is subject to license terms.
 */
package com.google.code.ei4j;

import com.google.code.ei4j.exceptions.UnableToReadImageException;
import com.google.code.ei4j.exceptions.UnableToWriteImageException;
import com.google.code.ei4j.exceptions.UnknownInputFormatException;
import com.google.code.ei4j.exceptions.UnknownOutputFormatException;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;

/**
 * Class that lets you chain method calls to scale and convert an image.
 * <p/>
 * This class is NOT thread safe.
 */
public class EasyImage {
    private InputStream stream;
    private Operation scaleOperation;
    private BufferedImage originalImage;

    private EasyImage(InputStream stream) {
        this.stream = stream;
    }

    /**
     * Create an EasyImage from a stream.
     * </p>
     * The stream isnt actually read until you apply an outputstream to write to.
     *
     * @param stream input stream
     * @return easy image
     */
    public static EasyImage fromStream(InputStream stream) {
        return new EasyImage(stream);
    }

    /**
     * Scale the image to the target height and width using a fast algorithm.
     * Will override any previous scaling done.
     *
     * @param targetWidth  target width
     * @param targetHeight target height
     * @param keepAspect   keep the image aspect
     * @return image for chaining calls.
     */
    public EasyImage scaleFast(int targetWidth, int targetHeight, boolean keepAspect) {
        scaleOperation =
                new ScaleOperation(targetWidth, targetHeight, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR,
                        false, keepAspect);
        return this;
    }

    /**
     * Scale the image to the target height and width using a high quality algorithm.
     * Will override any previous scaling done.
     *
     * @param targetWidth  target width
     * @param targetHeight target height
     * @param keepAspect   keep the image aspect
     * @return image for chaining calls.
     */
    public EasyImage scaleQuality(int targetWidth, int targetHeight, boolean keepAspect) {
        scaleOperation =
                new ScaleOperation(targetWidth, targetHeight, RenderingHints.VALUE_INTERPOLATION_BILINEAR, true,
                        keepAspect);
        return this;
    }

    /**
     * Write the image as a jpeg to the given outputstream.
     * Uses a compression quality of 0.9
     *
     * @param stream stream where the image should be written.
     * @throws com.google.code.ei4j.exceptions.UnknownOutputFormatException
     *                                     if format not known
     * @throws UnableToReadImageException  if not able to read input
     * @throws UnableToWriteImageException if not able to write output
     */
    public void toJpeg(OutputStream stream) {
        BufferedImage image = applyOperations();
        writeToFormat("JPEG", image, stream, 0.9f);
    }

    /**
     * Write the image as a jpeg to the given outputstream.
     *
     * @param stream  stream where the image should be written.
     * @param quality jpeg compression 0 to 1 where 1 is no compression. If you dont want compression, png might be a better choice
     * @throws com.google.code.ei4j.exceptions.UnknownOutputFormatException
     *                                     if format not known
     * @throws UnableToReadImageException  if not able to read input
     * @throws UnableToWriteImageException if not able to write output
     */
    public void toJpeg(OutputStream stream, float quality) {
        BufferedImage image = applyOperations();
        writeToFormat("JPEG", image, stream, quality);
    }

    /**
     * Write the image as a png to the given outputstream.
     *
     * @param stream stream where the image should be written.
     * @throws UnknownOutputFormatException if format not known
     * @throws UnableToReadImageException   if not able to read input
     * @throws UnableToWriteImageException  if not able to write output
     */
    public void toPng(OutputStream stream) {
        BufferedImage image = applyOperations();
        writeToFormat("PNG", image, stream, 1);
    }

    /**
     * Write the image in the given format to the outputstream.
     *
     * @param format format supported by the system.
     * @param stream stream where the image should be written.
     * @throws com.google.code.ei4j.exceptions.UnknownOutputFormatException
     *                                     if format not known
     * @throws UnableToReadImageException  if not able to read input
     * @throws UnableToWriteImageException if not able to write output
     */
    public void toFormat(String format, OutputStream stream) {
        BufferedImage image = applyOperations();
        writeToFormat(format, image, stream, 1);
    }

    /**
     * List all the available formats on this system.
     *
     * @return formats
     */
    public static Collection<String> availableFormats() {
        return Arrays.asList(ImageIO.getWriterFormatNames());
    }


    private BufferedImage applyOperations() {
        if (originalImage == null) {
            originalImage = loadImage();
        }
        return scaleOperation.apply(originalImage);
    }

    private BufferedImage loadImage() {
        try {
            BufferedImage image = ImageIO.read(stream);
            if (image != null) {
                return image;
            } else {
                throw new UnknownInputFormatException();
            }
        } catch (IOException e) {
            throw new UnableToReadImageException(e);
        }
    }

    private void writeToFormat(String format, BufferedImage inputImage, OutputStream stream, float compression) {
        try {
            Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName(format);
            if (iter.hasNext()) {
                ImageWriter writer = iter.next();
                ImageWriteParam iwp = writer.getDefaultWriteParam();
                if (iwp.canWriteCompressed()) {
                    iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                    String[] types = iwp.getCompressionTypes();
                    if (types != null && types.length > 0) {
                        // choose the first compression type...
                        iwp.setCompressionType(types[0]);
                    }
                    iwp.setCompressionQuality(compression);
                }
                writer.setOutput(new MemoryCacheImageOutputStream(stream));
                IIOImage image = new IIOImage(inputImage, null, null);
                writer.write(null, image, iwp);
                writer.dispose();
            } else {
                throw new UnknownOutputFormatException(format);
            }
        } catch (IOException e) {
            throw new UnableToWriteImageException(e);
        }
    }

}
